﻿// Copyright 2013 Ronald Schlenker, www.technewlogic.de.
// 
// This file is part of Byond.
// 
// Byond is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
// 
// Byond is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with Byond.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows.Controls;
using Byond.HostObjects;
using Byond.Persistence;
using Fasterflect;

namespace Byond
{
	class MetaPatch : IDisposable
	{
		public MetaPatch(Patch patch, string memberName, MetaPatch parent)
		{
			_boundProperties = Enumerable.Empty<FieldAndPropWrapper>();

			Patch = patch;
			Parent = parent;
			MemberName = memberName ?? string.Empty;

			if (parent != null)
				parent.AddChild(this);
			
			Members = CreateMembers();
			
			_explicitPersistProperties = Members.Where(it => it.Persist).ToList();

			EnsembleFileName = patch.GetType()
				.GetCustomAttributes(false)
				.OfType<PatchMetadataAttribute>()
				.Single()
				.EnsembleFileName;
		}

		public Patch Patch { get; private set; }
		public string MemberName { get; private set; }
		public string EnsembleFileName { get; private set; }
		public MetaPatch Parent { get; private set; }
		public Control PatchControl { get; set; }
		public IEnumerable<FieldAndPropWrapper> Members { get; private set; }

		private readonly List<MetaPatch> _children = new List<MetaPatch>();
		public IEnumerable<MetaPatch> Children { get { return _children.ToList(); } }

		private IEnumerable<FieldAndPropWrapper> _boundProperties;

		private readonly IEnumerable<FieldAndPropWrapper> _explicitPersistProperties;
		public IEnumerable<FieldAndPropWrapper> PersistProperties
		{
			get
			{
				return _explicitPersistProperties
					.Concat(_boundProperties)
					.Distinct()
					.ToList();
			}
		}

		public string RootPath
		{
			get
			{
				var path = string.Empty;
				var current = this;
				while (true)
				{
					if (string.IsNullOrWhiteSpace(path))
						path = current.MemberName;
					else if (!string.IsNullOrWhiteSpace(current.MemberName))
						path = current.MemberName + "." + path;

					current = current.Parent;
					if (current == null)
						return path;
				}
			}
		}

		private IEnumerable<FieldAndPropWrapper> CreateMembers()
		{
			// TODO: Caching pro Typ

			var fields = Patch.GetType()
				.GetFields(
					BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
				.Select(it => new FieldAndPropWrapper(
					it.Name,
					it.FieldType,
					it.HasAttribute<PersistAttribute>(),
					index => it.FieldType.IsArray && index != null
						? ((Array)it.Get(Patch)).GetValue(index.Value)
						: it.Get(Patch),
					(value, index) =>
					{
						// IMP: Bessere Konvertierung machen, z.B. wenn das Ding ein double ist und UI mäßig einstring oder umgekehrt. Auch soll die Konvertierung für Arrays gehen.
						if (index == null)
						{
							if (!it.FieldType.IsInstanceOfType(value))
								value = Convert.ChangeType(value, it.FieldType);
							it.Set(Patch, value);
						}
						else
						{
							((Array)it.Get(Patch)).SetValue(value, index.Value);
						}
					}));

			var properties = Patch.GetType()
				.GetProperties(
					BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
				.Select(it => new FieldAndPropWrapper(
					it.Name,
					it.PropertyType,
					it.HasAttribute<PersistAttribute>(),
					it.GetGetMethod(true) != null
						? new Func<int?, object>(index => it.Get(Patch))
						: index => null,
					it.GetSetMethod(true) != null
						? new Action<object, int?>((value, index) =>
							{
								if (!it.PropertyType.IsInstanceOfType(value))
									value = Convert.ChangeType(value, it.PropertyType);
								it.Set(Patch, value);
							})
						: (value, index) => { }));

			return fields
				.Concat(properties)
				.ToList();
		}

		private void AddChild(MetaPatch child)
		{
			_children.Add(child);
		}

		public void Dispose()
		{
			Patch.Dispose();
		}

		/// <summary>
		/// A descendant can be a MetaPatch or a member of a MetaPatch (i.e. a FieldAndPropWrapper)
		/// </summary>
		public T FindDescendantByPath<T>(string path)
			where T : class
		{
			var exceptionMessage = "Could not find code target for path: " + path;

			var pathSteps = path.Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries);
			var currentInstance = this;
			for (var i = 0; i < pathSteps.Length; i++)
			{
				var isLastIteration = (i == pathSteps.Length - 1);
				var step = pathSteps[i];
				var lastInstance = currentInstance;
				currentInstance = currentInstance.Children.FirstOrDefault(it => it.MemberName == step);
				if (currentInstance == null)
				{
					// Kein Child gefunden: Kann aber sein, dass es ein Property ist!
					var member = lastInstance.Members.FirstOrDefault(it => it.BindingMatches(step));
					if (member is T && isLastIteration)
						return member as T;

					throw new Exception(exceptionMessage);
				}

				if (isLastIteration)
				{
					if (currentInstance is T)
						return currentInstance as T;

					throw new Exception(exceptionMessage);
				}
			}

			throw new Exception(exceptionMessage);
		}

		public void RegisterBoundProperties(IEnumerable<FieldAndPropWrapper> boundProperties)
		{
			_boundProperties = boundProperties
				.Where(it => Members.Contains(it))
				.Where(it => !it.IsAction)
				.ToList();
		}

		public PatchDto ToDto()
		{
			return new PatchDto
				{
					RootPath = RootPath,
					Properties = PersistProperties
						.Select(it => new PropertyDto(it.MemberName, it.GetValueMethod(null)))
						.ToArray()
				};
		}
	}
}
